package org.lundberg.podcasts;

import java.io.IOException;

import org.lundberg.podcasts.util.PodcastDB;
import org.lundberg.podcasts.util.Util;

import android.annotation.TargetApi;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.BroadcastReceiver;
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.content.SharedPreferences.Editor;
import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.media.AudioManager;
import android.media.AudioManager.OnAudioFocusChangeListener;
import android.media.MediaPlayer;
import android.media.MediaPlayer.OnBufferingUpdateListener;
import android.media.MediaPlayer.OnCompletionListener;
import android.media.MediaPlayer.OnErrorListener;
import android.os.Binder;
import android.os.Handler;
import android.os.IBinder;
import android.preference.PreferenceManager;
import android.telephony.PhoneStateListener;
import android.telephony.TelephonyManager;
import android.util.Log;
import android.widget.Toast;

public class PlayerService extends Service
	implements OnCompletionListener, OnBufferingUpdateListener, OnErrorListener, OnAudioFocusChangeListener, OnSharedPreferenceChangeListener
{
	public static final int NOTIFICATION_ID = 1;
	public static final String PLAY = "org.lundberg.podcasts.PLAY";
	public static final String STOP = "org.lundberg.podcasts.STOP";
	public static final String PLAYING = "org.lundberg.podcasts.PLAYING";
	public static final String STOPPED = "org.lundberg.podcasts.STOPPED";
	public static final String SEEK = "org.lundberg.podcasts.SEEK";
	public static final String PAUSED = "org.lundberg.podcasts.PAUSED";
	public static final String PLAY_OR_STOP = "org.lundberg.podcasts.PLAY_OR_STOP";
	
	private boolean headsetTimerRunning=false;
	private int headsetClickCount=1;
	private boolean mDucking = false;
	private final IBinder mBinder = new LocalBinder();
	private boolean mediaButtonSeek = true;
	private int revSeekAmount = 0;
	private int fwdSeekAmount = 0;
	public String state;
	public MediaPlayer mediaPlayer;
	public String item_guid;
	private int channel_id;
	private final Handler mHandler = new Handler();
	private final Runnable mHeadsetTimerTask = new Runnable() {
		@Override
		public void run() {
			Log.d("PlayerService", "mHeadsetTimerTask clicks: "+headsetClickCount);
			// Seek
			if (headsetClickCount > 1) {
				if (PlayerService.PLAYING == state) {
					seek((headsetClickCount > 2) ? revSeekAmount : fwdSeekAmount, false);
				}
			}
			// Play/Pause
			else {
				if (PlayerService.PLAYING == state) {
	        		stopInternal();
	        	}
	        	else {
	        		play(item_guid);
	        	}
			}
			
			headsetTimerRunning=false;
		}
	};
	private final Runnable mUpdateTimeTask = new Runnable() {
		@Override
		public void run() {
			
			if (PlayerService.PLAYING ==  state)
			{
				SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(getApplicationContext());
				
				Editor editor = settings.edit();
	            
				int position = mediaPlayer.getCurrentPosition();
	            
	            editor.putLong("position", position);
	            
	            editor.commit();
				
	            Log.d("PlayerService", "saving position - position: "+position+"; guid: "+item_guid);
	            
		    	mHandler.postDelayed(this, 15000);
			}
		}
	};
	
	/* This receiver listens for intents from the IntentHandler.
	 * The IntentHandler handles events from media buttons */
	private BroadcastReceiver mStatusListener = new BroadcastReceiver() {
        @Override
        public void onReceive(Context context, Intent intent) {
        	// the play/pause button was pressed
        	if (PlayerService.PLAY_OR_STOP.equals(intent.getAction())) {
        		
	        	Log.d("PlayerService", "onReceive PLAY_OR_STOP");
        		
	        	// KEYCODE_HEADSETHOOK
	        	if (intent.getBooleanExtra("headset", false))
	        	{
		        	if (!headsetTimerRunning) {
		        		headsetClickCount=1;
		        		headsetTimerRunning = true;
		        		mHandler.postDelayed(mHeadsetTimerTask, 4000);
		        	}
		        	else {
		        		headsetClickCount++;
		        	}
	        	}
	        	// KEYCODE_MEDIA_PLAY_PAUSE 
	        	else
	        	{
	        		if (PlayerService.PLAYING == state) {
		        		stopInternal();
		        	}
		        	else {
		        		play(item_guid);
		        	}
	        	}
        	}
        	// the IntentHandler received an AUDIO_BECOMING_NOISY intent
        	else if (PlayerService.STOP.equals(intent.getAction())) {
        		stopInternal();
        	}
        	// the next or previous button was pressed
        	else if (PlayerService.SEEK.equals(intent.getAction())
        			&& mediaButtonSeek) {
        			
    			int amount = fwdSeekAmount;
    			if (IntentHandler.PREVIOUS == intent.getIntExtra("direction", IntentHandler.PREVIOUS)) {
    				amount = revSeekAmount;
    			}
    			
    			seek(amount, false);
        	}
        }
	};
	private IntentFilter mIntentFilter;
	private TelephonyManager mTelephonyManager;
	private PhoneStateListener phoneStateListener = new PhoneStateListener() {
		@Override
        public void onCallStateChanged(int callState, String incomingNumber) {
        	
        	super.onCallStateChanged(callState, incomingNumber);
        	
            Log.d("PlayerService", "onCallStateChanged: callState: "+callState+"; state: "+state);
            
            // If the callState is IDLE, and we are currently PAUSED, resume
            if (callState == TelephonyManager.CALL_STATE_IDLE && state == PlayerService.PAUSED)
            {
            	// If we were paused due to an incoming call, resume playback
            	play(item_guid);
            }
            // If the callState is not Idle (OFF_HOOK or RINGING) and we're playing, pause
            else if (state == PlayerService.PLAYING)
            {
        		stopInternal();
        		state = PlayerService.PAUSED;	
            }
        }
    };
	
    
	@Override
	public IBinder onBind(Intent arg0) {
		return mBinder;
	}
	
	
	@Override
	public void onCreate()
	{
		super.onCreate();
		
		// Register PhoneStateListener 
        if (mTelephonyManager == null) {
            mTelephonyManager = (TelephonyManager) this.getSystemService(Context.TELEPHONY_SERVICE);
            mTelephonyManager.listen(phoneStateListener, PhoneStateListener.LISTEN_CALL_STATE);
        }
        
        if (mIntentFilter == null) {

            mIntentFilter = new IntentFilter();
            mIntentFilter.addAction(PlayerService.PLAY_OR_STOP);
            mIntentFilter.addAction(PlayerService.STOP);
            mIntentFilter.addAction(PlayerService.SEEK);
        }
    	
    	registerReceiver(mStatusListener, mIntentFilter);
    	
    	// Set up a listener to be notified of changes to the keep_old_items preference
        SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(getApplicationContext());
		sp.registerOnSharedPreferenceChangeListener(this);
		revSeekAmount = -(Integer.parseInt(sp.getString(PodcastPreferenceActivity.REV_SEEK_PREF, "15")) * 1000);
		fwdSeekAmount = Integer.parseInt(sp.getString(PodcastPreferenceActivity.FWD_SEEK_PREF, "30")) * 1000;
		
        
        /* Make sure the service starts in the STOPPED state
         */
        this.state = PlayerService.STOPPED;        
        
        
        // Cancel any notification (in case the service was killed in the middle of playback) 
        NotificationManager nMgr = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
        nMgr.cancel(NOTIFICATION_ID);
        
        
        /* If we were playing (the service was killed), get the most recent guid & position from preferences,
         * and save it to the database.
         * Future TODO: Resume playing?
         */
     	String prevState = sp.getString("state", PlayerService.STOPPED);
     	String guid = sp.getString("current_item_guid", null);
     	long position = sp.getLong("position", 0);
     	
     	Log.d("PlayerService", "onCreate - prevState: "+prevState+"; position: "+position+"; guid: "+guid);
     	
     	if (PlayerService.PLAYING.equals(prevState) && guid != null) {
     			
 			PodcastDB helper = new PodcastDB(getApplicationContext());
	    	SQLiteDatabase db = helper.getReadableDatabase();
	    	
	    	db.execSQL("update "+PodcastDB.ITEMS_TABLE
	    			+" set position="+position
	    			+" where guid='"+guid+"'");
	    	
	    	helper.close();
     	}
     	
	}
	
	
	
	@Override
	public int onStartCommand(Intent intent, int flags, int startId)
	{
		Log.d("PlayerService", "onStartCommand: "+item_guid+"; "+(intent != null ? intent.getAction() : ""));
		
		if (intent != null)
		{
			String action = intent.getAction();
			
			if (PLAY.equals(action))
			{
				play(intent.getStringExtra("guid"));
			}
			else if (SEEK.equals(action))
			{
				seek(intent.getIntExtra("seek", 0),
					 intent.getBooleanExtra("beep", true));
			}
		}
		
		return START_STICKY;
	}
	
	
	@SuppressWarnings("deprecation")
	@TargetApi(16)
	private void play(String guid)
	{
		// If we are already playing, stop the current item before we begin
		// playing the new one
		if (state == PlayerService.PLAYING) {
			stop();
		}
		
		
		// Make sure we have a guid.
		// If one wasn't provided, use the class's
		if (guid == null) {
			
			if (this.item_guid != null) {
				guid = this.item_guid;
			}
			else {
				Log.e("PlayerService", "play - ERROR! guid && item_guid == null!");
				return;
			}
		}
		else {
			
			this.item_guid = guid;
		}
		
		
		PodcastDB helper = new PodcastDB(getApplicationContext());
    	SQLiteDatabase db = helper.getReadableDatabase();
    	String title = null;
    	int position = 0;
    	
    	Cursor c = db.query(false, PodcastDB.ITEMS_TABLE, new String[] {"channel_id",
																		"title",
																		"position"},
                "guid=\""+item_guid+"\"", null, null, null, null, null);
    	
    	if (c.moveToFirst()) {
    	
    		this.channel_id = c.getInt(0);
    		title = c.getString(1);
    		position = c.getInt(2);
    	}
    	
    	c.close();
    	helper.close();
		
		
		mediaPlayer = new MediaPlayer();
        mediaPlayer.setOnCompletionListener(this);
        mediaPlayer.setOnBufferingUpdateListener(this);
        mediaPlayer.setOnErrorListener(this);
        mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
		
		String localFile = null;
		
		if (Util.isDownloaded(item_guid)) {
			
			localFile = Util.getPodcastFilename(item_guid);
		}
		
		if (localFile == null) {
			
			Toast.makeText(getApplicationContext(), "Unable to play "+title, Toast.LENGTH_SHORT).show();
			Log.d("PlayerService", "localFile is null");
			return;
		}
		
		
		// Request Audio Focus
		AudioManager mAudioManager = (AudioManager)getSystemService(Context.AUDIO_SERVICE);        
    	mAudioManager.requestAudioFocus(this, AudioManager.STREAM_NOTIFICATION, AudioManager.AUDIOFOCUS_GAIN);
    	mAudioManager.requestAudioFocus(this, AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN);
		
		
		Log.d("PlayerService", "creating notification intent with guid: "+item_guid);
		
		String channelTitle = PodcastDB.getChannelTitle(getApplicationContext(), channel_id);
		String imageURL = PodcastDB.getImageURL(getApplicationContext(), channel_id);
        
        Intent notificationIntent = new Intent(this, PlayerActivity.class);
        PendingIntent contentIntent = PendingIntent.getActivity(this, 0, notificationIntent, 0);
        
        
        String iconFilename = Util.getIconFilename(imageURL);
        
        BitmapFactory.Options options = new BitmapFactory.Options();
    	options.inJustDecodeBounds = true;
    	BitmapFactory.decodeFile(iconFilename, options);
    	
    	int width = getResources().getDimensionPixelSize(android.R.dimen.notification_large_icon_width);
    	int height = getResources().getDimensionPixelSize(android.R.dimen.notification_large_icon_height);
    	
    	options.inSampleSize = Util.calculateInSampleSize(options, width, height);
    	options.inJustDecodeBounds = false;
    	
    	Bitmap largeIcon = BitmapFactory.decodeFile(iconFilename, options);
        
        Notification notification = null;
        
        Notification.Builder builder = new Notification.Builder(getApplicationContext())
		    .setContentTitle(channelTitle)
		    .setContentText(title)
		    .setSmallIcon(R.drawable.stat_play)
		    .setLargeIcon(largeIcon)
		    .setContentIntent(contentIntent);
        
        try
        {
        	notification = builder.build();
        }
        catch (NoSuchMethodError e) {
        	
        	notification = builder.getNotification();
        }
	
        notification.flags |= Notification.FLAG_ONGOING_EVENT | Notification.FLAG_NO_CLEAR;
        
        this.startForeground(NOTIFICATION_ID, notification);
        
        SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(getApplicationContext());
        
        if (position <= 0)
        {
        	String initialSkipString = settings.getString(PodcastPreferenceActivity.INITIAL_SKIP_PREF, "0");
        	position = Integer.parseInt(initialSkipString)*1000;
        }
        
        try
        {
        	mediaPlayer.setDataSource(localFile);
            mediaPlayer.prepare();
            mediaPlayer.seekTo(position);
            mediaPlayer.start();
            
            this.state = PlayerService.PLAYING;
            
            Intent playingIntent = new Intent(PlayerService.PLAYING);
            playingIntent.putExtra("next_item_guid", this.item_guid);
            sendBroadcast(playingIntent);
            
            Editor editor = settings.edit();
            editor.putString("current_item_guid", item_guid);
            editor.putString("state", this.state);
            editor.commit();
            
            // start the timer to record the position
            mHandler.postDelayed(mUpdateTimeTask, 15000);
        }
        catch (IllegalStateException e) {
            e.printStackTrace();
        }
        catch (IOException e) {
            e.printStackTrace();
        }
	}
	
	
	
	public void stop()
	{
		Log.d("PlayerService", "stop - item_guid: "+item_guid);
		
		mHandler.removeCallbacks(mUpdateTimeTask);
		
		if (PlayerService.PLAYING == state) {
			
			int progress = mediaPlayer.getCurrentPosition();
	    	int duration = mediaPlayer.getDuration();
			
        	mediaPlayer.stop();
        	mediaPlayer.release();
        	mediaPlayer = null;
        	
        	/* TODO: Why am I saving the state here?  Isn't it going to be stale by the time the method finishes?
        	 * I seem to be saving the PLAYING state, but I change it later to STOPPED.  Hrmm.
        	 * Edit: 2/10/2012 Save the final STOPPED state instead (assume no errors occur below and
        	 * this.state will be set to STOPPED...)
        	 */
        	SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(getApplicationContext());
    		Editor editor = settings.edit();
            //editor.putString("state", this.state);
    		editor.putString("state", PlayerService.STOPPED);
            
            this.stopForeground(true);
            
            // Save position & duration
	    	PodcastDB helper = new PodcastDB(this);
	    	SQLiteDatabase db = helper.getReadableDatabase();
	    	ContentValues values = new ContentValues();
	    	
	    	if (progress > 0) {
	    		
	    		values.put("position", progress);
	    	}
	    	
	    	values.put("duration", Util.timeIntToString(duration));
			
	    	boolean listened = (progress/(float)duration > 0.99);
	    	
	    	// If we have completed playback, set the 'listened' value
	    	values.put("listened", (listened) ? 1 : 0);
	    	
	    	db.update(PodcastDB.ITEMS_TABLE, values, "guid='"+item_guid+"'", null);
	        
	    	values.clear();
	    	
	    	
	    	/* If we have completed playback, decrement the number of new_items in the Channels table
	    	 */
	    	if (listened) {
	    		
	    		Cursor c = db.query(false, PodcastDB.CHANNELS_TABLE, new String[] {"new_items"},
	                    "_id="+channel_id, null, null, null, null, null);
	        	
	        	if (c.moveToFirst()) {
	        		
	        		values.put("new_items", c.getInt(0) - 1);
	        	}
	        	
	        	c.close();
	        	
	        	db.update(PodcastDB.CHANNELS_TABLE, values, "_id="+channel_id, null);
	        	
	        	// Clear out the current_item_guid
	            editor.remove("current_item_guid");
	    	}
	    	
	    	editor.commit();
	    	
	        helper.close();
        }
		
		state = PlayerService.STOPPED;
        
        AudioManager mAudioManager = (AudioManager)getSystemService(Context.AUDIO_SERVICE);
		mAudioManager.abandonAudioFocus(this);
	}
	
	
	
	/**
	 * Stops the playing mediaPlayer.  This version sends out a
	 * STOPPED broadcast intent to inform the PlayerActivity that
	 * the player has stopped. 
	 */
	private void stopInternal()
	{
		Log.d("PlayerService", "stopInternal - item_guid: "+item_guid);
		
		stop();
		
		Intent intent = new Intent(PlayerService.STOPPED);
		intent.putExtra("guid", item_guid);
		sendBroadcast(intent);
	}
	
	
	
	@Override
	public void onDestroy()
	{
		super.onDestroy();
		
		Log.d("PlayerService", "onDestroy");
		
		stopInternal();
		
		if (mTelephonyManager != null) {
            mTelephonyManager.listen(phoneStateListener, PhoneStateListener.LISTEN_NONE);
        }
		
		if (mStatusListener != null) {
			unregisterReceiver(mStatusListener);
		}
	}
	
	
	@Override
	public boolean onUnbind(Intent intent)
	{
		// If all clients have disconnected and we are not playing,
		// stop the service
		if (PlayerService.PLAYING != state) {
			stopSelf();
		}
		
		return true;
	}

	
	@Override
    public void onCompletion(MediaPlayer mp) {
		
		Log.d("PlayerService", "onCompletion - position: "+mediaPlayer.getCurrentPosition());
		
		stopInternal();
    }
    
	
    @Override
    public void onBufferingUpdate(MediaPlayer arg0, int percent) {
    	
    }
    
    
    @Override
	public boolean onError(MediaPlayer mp, int what, int extra) {
		
    	Log.d("PlayerService", "onError ("+what+", "+extra+")");
    	
    	stopInternal();
    	
		return true;
	}
    
    
    
    @Override
    public void onAudioFocusChange(int focusChange)
    {
    	Log.d("PlayerService", "onAudioFocusChange("+focusChange+")");
    	
    	switch (focusChange)
    	{
    	case AudioManager.AUDIOFOCUS_GAIN:
    				if (PlayerService.PLAYING == state) {
    					if (mDucking) {
    						mDucking = false;
    						mediaPlayer.setVolume(1.0f, 1.0f);
    					}
    					else {
    						mediaPlayer.start();
    					}
    				}
    				break;
    				
    	case AudioManager.AUDIOFOCUS_LOSS:
    				if (PlayerService.PLAYING == state) {
    					stopInternal();
    				}
    				break;
    				
    	case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:
		    		if (PlayerService.PLAYING == state) {
						mediaPlayer.pause();
					}
					break;
					
    	case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:
    				if (PlayerService.PLAYING == state) {
    					mDucking = true;
    					mediaPlayer.setVolume(0.125f, 0.125f);
    				}
    				break;
    	}
    }
    

    
    /**
     * Class for clients to access.  Because we know this service always
     * runs in the same process as its clients, we don't need to deal with
     * IPC.
     */
    public class LocalBinder extends Binder {
        public PlayerService getService() {
            return PlayerService.this;
        }
    }
    
    
    @Override
	public void onSharedPreferenceChanged(SharedPreferences sp, String key) {
		
		if (PodcastPreferenceActivity.MEDIA_BUTTON_SEEK_PREF.equals(key)) {
		
			mediaButtonSeek = sp.getBoolean(key, true);
		}
		else if (PodcastPreferenceActivity.REV_SEEK_PREF.equals(key)) {
			
			revSeekAmount = -(Integer.parseInt(sp.getString(PodcastPreferenceActivity.REV_SEEK_PREF, "15")) * 1000);
		}
		else if (PodcastPreferenceActivity.FWD_SEEK_PREF.equals(key)) {
			
			fwdSeekAmount = Integer.parseInt(sp.getString(PodcastPreferenceActivity.FWD_SEEK_PREF, "30")) * 1000;
		}
	}
    
    
    @Override
    public void onLowMemory()
    {
    	int position = 0;
    	
    	if (PlayerService.PLAYING == state) {
    		position = mediaPlayer.getCurrentPosition();
    	}
    	
    	Log.d("PlayerService", "onLowMemory - position: "+position);
    	
    	super.onLowMemory();
    }
    
    
    
    private void seek(final int amount, final boolean beep)
    {
    	Log.d("PlayerService", "SEEK: "+amount);
    	
    	if (PlayerService.PLAYING == state) {
    		
    		new Thread() {
	    		public void run() {
	    			
	    			mediaPlayer.pause();
	    			
	    			int position = Math.min(mediaPlayer.getDuration()-1500,
	    									  amount + mediaPlayer.getCurrentPosition());
	    			
		    		mediaPlayer.seekTo(position);
		    		
		    		mHandler.postDelayed(new Runnable() {
		    			public void run() {
		    				if (mediaPlayer != null && state == PlayerService.PLAYING) {
		    					mediaPlayer.start();
		    				}
		    			}}, 1500);
	    		}
    		}.start();
    	}
    }
    
}
